Ten self-contained programming tasks that probe how well a local model performs as a coding assistant inside Aperio β from pure helper functions to cross-cutting store changes. Each task has a copy-paste prompt, a verify command, and clear pass criteria.
Originally two separate eval suites β a 3-task warm-up for gemma3:e4b and a 7-task progression for gemma4:e4b β merged into a single progressive test. Start at Task 1 and work up; the tasks reveal how deep the model's understanding goes.
Most tasks follow these project conventions. Knowing the layers helps you evaluate whether the model understood the right place to make a change.
| Layer | Location | Pattern |
|---|---|---|
| Routes | lib/routes/api-*.js | mount*Routes(router, { store }) β thin Express handlers |
| Handlers | lib/handlers/*/ | Pure async functions: handler(ctx, args) β independently testable |
| MCP Tools | mcp/tools/*.js | Zod schemas β bound handlers |
| Helpers | lib/helpers/*.js | Single-purpose utility functions |
| Store | db/*.js | DB abstraction (SQLite / Postgres) |
| Tests | tests/ | Node built-in runner: node:test + node:assert/strict |
You need two terminals β one for the Aperio server on your local llama.cpp provider, and one for the chat client where you'll paste each task prompt.
npm run start:local # AI_PROVIDER=llamacpp, port 31337
npm run chat:local
Paste each task's prompt into Terminal B. Use a fresh conversation per task when possible. After each task, run the Verify command in a third terminal (or stop the chat and use Terminal B).
If you want to re-run tasks against a different model, run this reset first:
git checkout -- lib/helpers/redactSecrets.js lib/helpers/imageTokens.js lib/terminal.js \
tests/lib/helpers/redactSecrets.test.js \
lib/routes/api.js db/sqlite.js db/postgres.js db/index.js
rm -f lib/helpers/truncateMiddle.js lib/helpers/slugify.js lib/helpers/truncateText.js \
tests/lib/helpers/truncateMiddle.test.js tests/helpers/slugify.test.js \
tests/helpers/truncateText.test.js \
lib/routes/api-health.js lib/routes/api-stats.js lib/routes/api-tags.js
Ordered by what they test: pure functions first, then surgical edits, Express routing, store/DB abstraction, application flow, and cross-cutting changes. Each task lists what files it touches, a difficulty badge, the prompt to paste, what to look for when evaluating, and the verify command.
Probes: basic code generation, string transformation, edge-case awareness
Files:1 new file (lib/helpers/slugify.js) + 1 test (tests/helpers/slugify.test.js)
lib/helpers/slugify.js that exports a slugify(str) function. It should convert any string to a URL-safe slug: lowercase, replace spaces and special chars with hyphens, collapse consecutive hyphens, trim leading/trailing hyphens. Also write a test at tests/helpers/slugify.test.js. Follow existing project patterns β use ES modules, the Node built-in test runner, and node:assert/strict.export function β¦)?node --test tests/helpers/slugify.test.jsProbes: length-guarantee discipline, off-by-one awareness, export style
Files:1 new file (lib/helpers/truncateMiddle.js) + 1 test (tests/lib/helpers/truncateMiddle.test.js)
lib/helpers/truncateMiddle.js that exports a function truncateMiddle(str, max). If str.length <= max, return str unchanged. Otherwise keep characters from the start and the end and put a single β¦ in the middle, so that the returned string's length is exactly max. Use ES module export. Then create tests/lib/helpers/truncateMiddle.test.js using node:test and assert/strict with cases for: a short string (unchanged), an exact-length string (unchanged), and a long string (length equals max, contains β¦).max (the classic off-by-one trap)β¦ is one character<= edge handled correctlynode --test tests/lib/helpers/truncateMiddle.test.jsProbes: word-boundary logic, length guarantees, defensive defaults
Files:1 new file (lib/helpers/truncateText.js) + 1 test (tests/helpers/truncateText.test.js)
lib/helpers/truncateText.js that exports truncateText(text, maxLength = 200, suffix = 'β¦'). It should truncate at a word boundary when possible, never cut mid-word unless a word is longer than maxLength, and always append the suffix when truncation occurs. The returned length (including suffix) must never exceed maxLength. Write a test at tests/helpers/truncateText.test.js. Use ES modules and the Node built-in test runner.node --test tests/helpers/truncateText.test.jsProbes: reading existing patterns, surgical edits, regex anchoring
Files:1 edit (lib/helpers/redactSecrets.js) + 1 test append (tests/lib/helpers/redactSecrets.test.js)
lib/helpers/redactSecrets.js, add a new rule to the RULES array that redacts Stripe secret keys. They look like sk_live_ followed by 24 or more alphanumeric characters. Replace each match with [REDACTED:stripe-key]. Then add a matching test in tests/lib/helpers/redactSecrets.test.js that follows the existing test style.RULES with kind stripe-key\b anchoringnode --test tests/lib/helpers/redactSecrets.test.jsProbes: surgical precision, touching only one case, commenting
Files:1 edit (lib/helpers/imageTokens.js)
lib/helpers/imageTokens.js, add a new case 'mistral': to the switch in imageTokenEstimate that returns 1024, with a one-line comment explaining the figure. Do not change any existing case or the default behavior.mistral β 1024node -e "import('./lib/helpers/imageTokens.js').then(m => { const assert = require('node:assert'); assert.strictEqual(m.imageTokenEstimate('mistral'), 1024); assert.strictEqual(m.imageTokenEstimate('llamacpp'), 256); assert.strictEqual(m.imageTokenEstimate('anthropic'), 1070); console.log('OK β mistral added, existing cases intact'); })"Probes: Express routing patterns, store integration, error handling
Files:1 new route file (lib/routes/api-health.js) + 1 edit (lib/routes/api.js)
GET /api/health endpoint. It should query the store to verify the database is reachable (e.g., run SELECT 1 or call a store method) and return:{"status": "ok", "db": "connected", "uptime": <process.uptime seconds>}{"status": "error"}. Mount it via a new mountHealthRoutes(router, { store }) function in lib/routes/api-health.js, and wire it into lib/routes/api.js. Follow existing route patterns exactly (see lib/routes/api-wiki.js for an example).mount*Routes(router, { store }) convention?api.js?npm start &
curl http://localhost:3000/api/healthProbes: store abstraction, aggregate queries, SQLite vs Postgres split
Files:1 new route file (lib/routes/api-stats.js) + edits to db/sqlite.js, db/postgres.js, db/index.js, lib/routes/api.js
GET /api/stats endpoint that returns aggregate counts from the database. It should return:{"memories": {"total": 42, "by_type": {"fact": 20, "preference": 12, "event": 10}}, "wiki_articles": {"total": 8, "by_status": {"fresh": 5, "stale": 2, "draft": 1}}, "agent_jobs": {"total": 3}}getStats() method to the store interface in db/index.js (which delegates to the active backend). For SQLite, implement it in db/sqlite.js with real queries against the memories, wiki_articles, and agent_jobs tables. For Postgres, add a stub in db/postgres.js that throws "not implemented". Create the route in lib/routes/api-stats.js (following the mount*Routes pattern) and wire it into lib/routes/api.js. Follow existing patterns.npm start &
curl http://localhost:3000/api/stats | jqProbes: prefix search, parameterized queries, input safety
Files:1 new route file (lib/routes/api-tags.js) + edits to db/sqlite.js, db/postgres.js, db/index.js, lib/routes/api.js
GET /api/tags/suggest?q=<prefix> endpoint. It should query all distinct tags from the memories table that start with the given prefix (case-insensitive), ordered by frequency (most-used first), limited to 10 results. Return:{"tags": ["deployment", "debugging", "design"]}suggestTags(prefix, limit) method to the store (SQLite implementation, Postgres stub). Create the route in lib/routes/api-tags.js and mount it in lib/routes/api.js. Follow existing patterns. If q is missing or empty, return all tags ordered by frequency (top 20).npm start &
curl "http://localhost:3000/api/tags/suggest?q=de"Probes: understanding existing app flow, surgical CLI changes
Files:1 edit (lib/terminal.js)
npm run chat:local) starts via lib/terminal.js. Add a --no-memory CLI flag. When passed, the agent session should skip preloading memories from the store. Parse it from process.argv (e.g., --no-memory as a boolean flag β present means skip, absent means load normally). Make the change minimal β don't refactor unrelated code. Follow existing patterns in the file.npm run chat:local -- --no-memoryProbes: backwards compatibility, cross-cutting changes, MCP handler safety
Files:Edits to lib/routes/api-memories.js, db/sqlite.js, db/postgres.js
GET /api/memories endpoint currently returns {"memories": [...]}. Enhance it to include pagination metadata:{"memories": [...], "pagination": {"total": 150, "limit": 20, "offset": 0, "has_more": true}}{rows, total} object instead of just an array. Update the SQLite implementation in db/sqlite.js to run a COUNT(*) OVER() window query or a separate count. Update the Postgres stub similarly. Update the route in lib/routes/api-memories.js to include the pagination wrapper.recall handler β that should still receive just the rows array (call .rows on the result). Follow existing patterns.npm testnpm run chat:local β recall some topicApply these five signals to each task after running the verify command. They measure not just "does it work" but also "was the model surgical" and "did it actually verify its own work".
| Signal | 0 = fail | 1 = pass |
|---|---|---|
| Edited the correct file | wrong file / new file when edit expected | right file |
| Verify command passes | red / errors | green |
| Stayed surgical (no unrelated rewrites) | rewrote / "improved" unrelated code | minimal diff |
| Produced the test when asked | code only | code + test |
| Ran/read the verify itself when prompted | guessed / claimed without checking | actually ran / read output |
Results fill in automatically as you click the buttons above.
| Test | Result | Notes |
|---|---|---|
| 1. slugify() Helper | β | |
| 2. truncateMiddle() Helper | β | |
| 3. truncateText() Helper | β | |
| 4. redactSecrets β Stripe Key | β | |
| 5. imageTokens β Mistral Case | β | |
| 6. Health Check Endpoint | β | |
| 7. Memory Stats Endpoint | β | |
| 8. Tag Suggestions Endpoint | β | |
| 9. --no-memory CLI Flag | β | |
| 10. Pagination Metadata | β | |
| TOTAL: ____ / 10 passed | ||
The task order traces a curve from shallow to deep understanding:
pure functions (tasks 1β3) β surgical existing-code edits (4β5) β Express routing (6) β store/DB abstraction (7β8) β application flow (9) β cross-cutting changes (10)
A model that drops off after Task 5 knows JavaScript but not architecture. One that drops off after Task 8 knows the stack but struggles with cross-cutting safety. A model that passes all 10 understands the project deeply.